const CryptoJS = require('crypto-js');

/**
 * Utility class for AES encryption and decryption
 */
class AESUtil {
  /**
   * Encrypts the input data using AES-128 CBC with NoPadding.
   * @param {Uint8Array} data Plain byte array. Length must be a multiple of 16 bytes.
   * @param {Uint8Array} key 16-byte AES key.
   * @param {Uint8Array} iv 16-byte initialization vector.
   * @return {Uint8Array} Encrypted data.
   */
  static encrypt(data, key, iv) {
    if (key.length !== 16) {
      throw new Error("Key length must be 16 bytes (128 bits)");
    }
    if (iv.length !== 16) {
      throw new Error("IV length must be 16 bytes");
    }
    if (data.length % 16 !== 0) {
      throw new Error("Data length must be a multiple of 16 bytes (NoPadding mode)");
    }

    // Convert Uint8Array to WordArray
    const dataWords = this._uint8ArrayToWordArray(data);
    const keyWords = this._uint8ArrayToWordArray(key);
    const ivWords = this._uint8ArrayToWordArray(iv);

    // Encrypt using AES-CBC with no padding
    const encrypted = CryptoJS.AES.encrypt(
      dataWords, 
      keyWords, 
      {
        iv: ivWords,
        padding: CryptoJS.pad.NoPadding,
        mode: CryptoJS.mode.CBC
      }
    );

    // Convert WordArray back to Uint8Array
    return this._wordArrayToUint8Array(encrypted.ciphertext);
  }

  /**
   * Decrypts the input data using AES-128 CBC with NoPadding.
   * @param {Uint8Array} encryptedData Encrypted byte array. Length must be a multiple of 16 bytes.
   * @param {Uint8Array} key 16-byte AES key.
   * @param {Uint8Array} iv 16-byte initialization vector.
   * @return {Uint8Array} Decrypted data.
   */
  static decrypt(encryptedData, key, iv) {
    if (key.length !== 16) {
      throw new Error("Key length must be 16 bytes (128 bits)");
    }
    if (iv.length !== 16) {
      throw new Error("IV length must be 16 bytes");
    }
    if (encryptedData.length % 16 !== 0) {
      throw new Error("Encrypted data length must be a multiple of 16 bytes (NoPadding mode)");
    }

    // Convert Uint8Array to WordArray
    const encryptedWords = this._uint8ArrayToWordArray(encryptedData);
    const keyWords = this._uint8ArrayToWordArray(key);
    const ivWords = this._uint8ArrayToWordArray(iv);

    // Create CipherParams object
    const cipherParams = CryptoJS.lib.CipherParams.create({
      ciphertext: encryptedWords
    });

    // Decrypt using AES-CBC with no padding
    const decrypted = CryptoJS.AES.decrypt(
      cipherParams,
      keyWords,
      {
        iv: ivWords,
        padding: CryptoJS.pad.NoPadding,
        mode: CryptoJS.mode.CBC
      }
    );

    // Convert WordArray back to Uint8Array
    return this._wordArrayToUint8Array(decrypted);
  }

  /**
   * Encrypts a list of unsigned integers.
   * @param {Array<number>} commandList List of unsigned integers.
   * @param {number} fromIndex Start index (inclusive).
   * @param {number} toIndex End index (inclusive).
   * @param {Uint8Array} key 16-byte AES key.
   * @param {Uint8Array} iv 16-byte initialization vector.
   * @return {Array<number>} Encrypted list of unsigned integers.
   */
  static encryptUInt(commandList, fromIndex, toIndex, key, iv) {
    if (fromIndex < 0 || fromIndex >= commandList.length) {
      throw new Error("fromIndex out of bounds");
    }
    if (toIndex < 0 || toIndex >= commandList.length) {
      throw new Error("toIndex out of bounds");
    }
    if (fromIndex > toIndex) {
      throw new Error("fromIndex must be <= toIndex");
    }
    
    const rangeSize = toIndex - fromIndex + 1;
    if ((rangeSize * 4) % 16 !== 0) {
      throw new Error("The byte size of selected range must be a multiple of 16 for AES NoPadding");
    }

    // Convert UInt array to byte array
    const buffer = new ArrayBuffer(rangeSize * 4);
    const dataView = new DataView(buffer);
    for (let i = 0; i < rangeSize; i++) {
      dataView.setUint32(i * 4, commandList[fromIndex + i], false); // big-endian
    }

    // Encrypt the byte array
    const encrypted = this.encrypt(new Uint8Array(buffer), key, iv);
    
    // Convert encrypted bytes back to UInt array
    const encryptedUInts = [];
    const encryptedView = new DataView(encrypted.buffer);
    for (let i = 0; i < rangeSize; i++) {
      encryptedUInts.push(encryptedView.getUint32(i * 4, false)); // big-endian
    }

    // Build the result list
    return [
      ...commandList.slice(0, fromIndex),
      ...encryptedUInts,
      ...commandList.slice(toIndex + 1)
    ];
  }

  /**
   * Decrypts a list of unsigned integers.
   * @param {Array<number>} commandList List of unsigned integers.
   * @param {number} fromIndex Start index (inclusive).
   * @param {number} toIndex End index (inclusive).
   * @param {Uint8Array} key 16-byte AES key.
   * @param {Uint8Array} iv 16-byte initialization vector.
   * @return {Array<number>} Decrypted list of unsigned integers.
   */
  static decryptUInt(commandList, fromIndex, toIndex, key, iv) {
    if (fromIndex < 0 || fromIndex >= commandList.length) {
      throw new Error("fromIndex out of bounds");
    }
    if (toIndex < 0 || toIndex >= commandList.length) {
      throw new Error("toIndex out of bounds");
    }
    if (fromIndex > toIndex) {
      throw new Error("fromIndex must be <= toIndex");
    }
    
    const rangeSize = toIndex - fromIndex + 1;
    if ((rangeSize * 4) % 16 !== 0) {
      throw new Error("The byte size of selected range must be a multiple of 16 for AES NoPadding");
    }

    // Convert UInt array to byte array
    const buffer = new ArrayBuffer(rangeSize * 4);
    const dataView = new DataView(buffer);
    for (let i = 0; i < rangeSize; i++) {
      dataView.setUint32(i * 4, commandList[fromIndex + i], false); // big-endian
    }

    // Decrypt the byte array
    const decrypted = this.decrypt(new Uint8Array(buffer), key, iv);
    
    // Convert decrypted bytes back to UInt array
    const decryptedUInts = [];
    const decryptedView = new DataView(decrypted.buffer);
    for (let i = 0; i < rangeSize; i++) {
      decryptedUInts.push(decryptedView.getUint32(i * 4, false)); // big-endian
    }

    // Build the result list
    return [
      ...commandList.slice(0, fromIndex),
      ...decryptedUInts,
      ...commandList.slice(toIndex + 1)
    ];
  }

  /**
   * Converts a Uint8Array to a CryptoJS WordArray.
   * @private
   * @param {Uint8Array} uint8Array The input Uint8Array.
   * @return {CryptoJS.lib.WordArray} The resulting WordArray.
   */
  static _uint8ArrayToWordArray(uint8Array) {
    const words = [];
    for (let i = 0; i < uint8Array.length; i += 4) {
      words.push(
        (uint8Array[i] << 24) |
        ((i + 1 < uint8Array.length ? uint8Array[i + 1] : 0) << 16) |
        ((i + 2 < uint8Array.length ? uint8Array[i + 2] : 0) << 8) |
        (i + 3 < uint8Array.length ? uint8Array[i + 3] : 0)
      );
    }
    return CryptoJS.lib.WordArray.create(words, uint8Array.length);
  }

  /**
   * Converts a CryptoJS WordArray to a Uint8Array.
   * @private
   * @param {CryptoJS.lib.WordArray} wordArray The input WordArray.
   * @return {Uint8Array} The resulting Uint8Array.
   */
  static _wordArrayToUint8Array(wordArray) {
    const words = wordArray.words;
    const sigBytes = wordArray.sigBytes;
    const result = new Uint8Array(sigBytes);
    
    for (let i = 0; i < sigBytes; i++) {
      const byte = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
      result[i] = byte;
    }
    
    return result;
  }
}

module.exports = AESUtil;
